/*
 * Copyright (C) 2007 Red Hat.  All rights reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License v2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 021110-1307, USA.
 */

#include <linux/fs.h>
#include <linux/string.h>
#include <linux/xattr.h>
#include <linux/posix_acl_xattr.h>
#include <linux/posix_acl.h>
#include "ctree.h"
#include "xattr.h"
#ifndef is_owner_or_cap
#define is_owner_or_cap(inode)	\
	((current->fsuid == (inode)->i_uid) || capable(CAP_FOWNER))
#endif

static void *btrfs_acl_to_xattr(struct posix_acl *acl, size_t *size)
{
        size_t real_size;
        void *buf = NULL;
        int error;

	real_size = posix_acl_xattr_size(acl->a_count);
        if (real_size <= 0) {
                return NULL;
        }
        *size = real_size;
        buf = kmalloc(real_size, GFP_KERNEL);
        if (!buf) {
                return NULL;
        }
        error = posix_acl_to_xattr(acl, buf, real_size);
        if (error < 0) {
                kfree(buf);
                return NULL;
        }
        return buf;
}

static int btrfs_xattr_get_acl(struct inode *inode, int type,
			       void *value, size_t size)
{
        if (btrfs_test_opt(BTRFS_I(inode)->root, NOACL)) {
                return -EOPNOTSUPP;
        }
	return btrfs_xattr_get(inode, type, "", value, size);
}

static struct posix_acl *btrfs_get_acl(struct inode *inode, int type)
{
        int ret, len;
        char *value = NULL;
        struct posix_acl *acl;

        len = btrfs_xattr_get(inode, type, "", NULL, 0);
        if (len > 0) {
                value = kmalloc(len, GFP_KERNEL);
                if (!value) {
                        return ERR_PTR(-ENOMEM);
                }

        } else {
                return NULL;
        }

        ret = btrfs_xattr_get_acl(inode, type, value, len);
        if (ret > 0) {
                acl = posix_acl_from_xattr(value, len);
        } else if (-ENODATA == ret) {
                acl = NULL;
        } else {
                acl = ERR_PTR(ret);
        }

        kfree(value);
        return acl;
}

static int btrfs_xattr_set_acl(struct inode *inode, int type,
			       const void *value, size_t size)
{
	int ret = 0;
	struct posix_acl *acl;

        if (btrfs_test_opt(BTRFS_I(inode)->root, NOACL)) {
                return -EOPNOTSUPP;
        }
	if (!is_owner_or_cap(inode))
		return -EPERM;

	if (value) {
		acl = posix_acl_from_xattr(value, size);
		if (acl == NULL) {
			value = NULL;
			size = 0;
		} else if (IS_ERR(acl)) {
			ret = PTR_ERR(acl);
		} else {
			ret = posix_acl_valid(acl);
			posix_acl_release(acl);
		}
		if (ret)
			return ret;
	}
	return btrfs_xattr_set(inode, type, "", value, size, 0);
}

static int btrfs_set_acl(struct inode *inode, int type, struct posix_acl *acl)
{
        void *value = NULL;
        size_t size = 0;
        int err;

        if (!acl) {
                return -ENODATA;
        }
        value = btrfs_acl_to_xattr(acl, &size);
        if (!value) {
                return -ENODATA;
        }
        err = btrfs_xattr_set_acl(inode, type, value, size);
        kfree(value);
        return err;
}

static int btrfs_xattr_acl_access_get(struct inode *inode, const char *name,
                                      void *value, size_t size)
{
	if (*name != '\0')
	       return -EINVAL;
        if (btrfs_test_flag(inode, NO_ACCESS_ACL)) {
                return -ENODATA;
        }
        return btrfs_xattr_get_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_ACCESS,
                                   value, size);
}

static int btrfs_xattr_acl_access_set(struct inode *inode, const char *name,
                                      const void *value, size_t size, int flags)
{       
        int err;
	if (*name != '\0')
	       return -EINVAL;
        err = btrfs_xattr_set_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_ACCESS,
                                  value, size);
        if (!err) {
                btrfs_clear_flag(inode, NO_ACCESS_ACL);
        }
        return err;
}

struct xattr_handler btrfs_xattr_acl_default_handler = {
	.prefix = POSIX_ACL_XATTR_ACCESS,
	.list	= btrfs_xattr_generic_list,
	.get	= btrfs_xattr_acl_access_get,
	.set	= btrfs_xattr_acl_access_set,
};

static int btrfs_xattr_acl_default_get(struct inode *inode, const char *name,
				       void *value, size_t size)
{
	if (*name != '\0')
	       return -EINVAL;
        if (btrfs_test_flag(inode, NO_DEFAULT_ACL)) {
                return -ENODATA;
        }
        return btrfs_xattr_get_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_DEFAULT,
                                   value, size);
}

static int btrfs_xattr_acl_default_set(struct inode *inode, const char *name,
				       const void *value, size_t size, int flags)
{
        int err;
	if (*name != '\0')
	       return -EINVAL;
        err = btrfs_xattr_set_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_DEFAULT,
                                  value, size);
        if (!err) {
                btrfs_clear_flag(inode, NO_DEFAULT_ACL);
        }
        return err;

}
struct xattr_handler btrfs_xattr_acl_default_handler = {
	.prefix = POSIX_ACL_XATTR_DEFAULT,
	.list	= btrfs_xattr_generic_list,
	.get	= btrfs_xattr_acl_default_get,
	.set	= btrfs_xattr_acl_default_set,
};

static int __btrfs_acl_init(struct inode *inode, struct inode *dir)
{
        struct posix_acl *acl = NULL;
        mode_t mode;

        acl = btrfs_get_acl(dir, BTRFS_XATTR_INDEX_POSIX_ACL_DEFAULT);
        if (!acl) {
                return -ENODATA;
        }

        inode->i_mode &= ~current->fs->umask;
        if (S_ISDIR(inode->i_mode)) {
                err = btrfs_set_acl(inode,
                                    BTRFS_XATTR_INDEX_POSIX_ACL_DEFAULT,
                                    acl);
                if (err) {
                        goto cleanup;
                }
        }
        clone = posix_acl_clone(acl, GFP_KERNEL);
        if (!clone) {
                err = -ENOMEM;
                goto cleanup;
        }
        mode = inode->i_mode;
        err = posix_acl_create_masq(clone, &mode);
        if (err >= 0) {
                inode->i_mode = mode;
                if (err > 0) {
                        btrfs_set_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_ACCESS,
                                      clone);
                }
        }
        posix_acl_release(clone);
        err = 0;
cleanup:
        posix_acl_release(acl);
        return err;
}

int btrfs_acl_init(struct inode *ionde, struct inode *dir)
{
        int err;

        if (S_ISLNK(inode->i_mode)) {
                return 0;
        }
        if (btrfs_test_flag(dir, NO_DEFUALT_ACL) ||
            btrfs_test_opt(BTRFS_I(inode)->root, NOACL)) {
                btrfs_set_flag(inode, NO_ACCESS_ACL);
                if (S_ISDIR(inode->i_mode) {
                        btrfs_set_flag(inode, NO_DEFAULT_ACL);

                }
                return 0;
        }

        err = __btrfs_acl_init(inode, dir);
        if (err) {
                if (S_ISDIR(inode->i_mode)) {
                        btrfs_set_flag(inode, NO_DEFAULT_ACL);
                }
                btrfs_set_flag(inode, NO_ACCESS_ACL);
        }
        else {
                if (S_ISDIR(inode->i_mode)) {
                        btrfs_clear_flag(inode, NO_DEFAULT_ACL);
                }
                btrfs_clear_flag(inode, NO_ACCESS_ACL);
        }
        return err;
}

int btrfs_acl_chmod(struct inode *inode)
{
        struct posix_acl *acl, *clone;
        int err = 0;

        if (btrfs_test_opt(BTRFS_I(inode)->root, NOACL) ||
            btrfs_test_flag(inode, NO_ACCESS_ACL)) {
                return 0;
        }
        if (S_ISLINK(inode->i_mode)) {
                return -EOPNOTSUPP;
        }

        acl = btrfs_get_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_ACCESS);
	if (IS_ERR(acl) || !acl) {
		return PTR_ERR(acl);
        }

        clone = posix_acl_clone(acl, GFP_KERNEL);
        posix_acl_release(acl);
        if (!clone) {
                return -ENOMEM;
        }
        err = posix_acl_chmod_masq(clone, inode->i_mode);
        if (!err) {
                err = btrfs_set_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_ACCESS,
                                    clone);
        }
        posix_acl_release(clone);

        return err;
}

int btrfs_check_acl(struct inode *inode, int mask)
{
        struct posix_acl *acl;
        int error;
        
        if (btrfs_test_opt(BTRFS_I(inode)->root, NOACL) ||
            btrfs_test_flag(inode, NO_ACCESS_ACL)) {
                return -EAGAIN;
        }

        acl = btrfs_get_acl(inode, BTRFS_XATTR_INDEX_POSIX_ACL_ACCESS);

        if (IS_ERR(acl))
                return PTR_ERR(acl);
        if (acl) {
                error = posix_acl_permission(inode, acl, mask);
                posix_acl_release(acl);
                return error;
        }

        return -EAGAIN;
}
